using System.Collections.Generic;
using System.Linq;
using ArchUnitNET.Domain;
using ArchUnitNET.Domain.Extensions;
using ArchUnitNET.Loader.LoadTasks;
using JetBrains.Annotations;
using Mono.Cecil;
using GenericParameter = ArchUnitNET.Domain.GenericParameter;

namespace ArchUnitNET.Loader
{
    internal class ArchBuilder
    {
        private readonly ArchitectureCache _architectureCache;
        private readonly ArchitectureCacheKey _architectureCacheKey;
        private readonly IDictionary<string, IType> _architectureTypes =
            new Dictionary<string, IType>();
        private readonly AssemblyRegistry _assemblyRegistry;
        private readonly LoadTaskRegistry _loadTaskRegistry;
        private readonly NamespaceRegistry _namespaceRegistry;
        private readonly TypeFactory _typeFactory;

        public ArchBuilder()
        {
            _assemblyRegistry = new AssemblyRegistry();
            _namespaceRegistry = new NamespaceRegistry();
            _loadTaskRegistry = new LoadTaskRegistry();
            _typeFactory = new TypeFactory(
                _loadTaskRegistry,
                _assemblyRegistry,
                _namespaceRegistry
            );
            _architectureCacheKey = new ArchitectureCacheKey();
            _architectureCache = ArchitectureCache.Instance;
        }

        public IEnumerable<IType> Types => _architectureTypes.Values;
        public IEnumerable<Assembly> Assemblies => _assemblyRegistry.Assemblies;
        public IEnumerable<Namespace> Namespaces => _namespaceRegistry.Namespaces;

        public void AddAssembly([NotNull] AssemblyDefinition moduleAssembly, bool isOnlyReferenced)
        {
            var references = moduleAssembly
                .MainModule.AssemblyReferences.Select(reference => reference.Name)
                .ToList();

            if (!_assemblyRegistry.ContainsAssembly(moduleAssembly.FullName))
            {
                var assembly = _assemblyRegistry.GetOrCreateAssembly(
                    moduleAssembly.Name.Name,
                    moduleAssembly.FullName,
                    isOnlyReferenced,
                    references
                );
                _loadTaskRegistry.Add(
                    typeof(CollectAssemblyAttributes),
                    new CollectAssemblyAttributes(assembly, moduleAssembly, _typeFactory)
                );
            }
        }

        public void LoadTypesForModule(ModuleDefinition module, string namespaceFilter)
        {
            _architectureCacheKey.Add(module.Name, namespaceFilter);

            var types = module.Types.First().FullName.Contains("<Module>")
                ? module.Types.Skip(1).ToList()
                : module.Types.ToList();

            types = types
                .Where(t =>
                    t.FullName != "Microsoft.CodeAnalysis.EmbeddedAttribute"
                    && t.FullName != "System.Runtime.CompilerServices.NullableAttribute"
                    && t.FullName != "System.Runtime.CompilerServices.NullableContextAttribute"
                    && !t.FullName.StartsWith("Coverlet")
                )
                .ToList();

            var nestedTypes = types;
            while (nestedTypes.Any())
            {
                nestedTypes = nestedTypes
                    .SelectMany(typeDefinition =>
                        typeDefinition.NestedTypes.Where(type => !type.IsCompilerGenerated())
                    )
                    .ToList();
                types.AddRange(nestedTypes);
            }

            var currentTypes = new List<IType>(types.Count);
            types
                .Where(typeDefinition =>
                    RegexUtils.MatchNamespaces(namespaceFilter, typeDefinition.Namespace)
                    && typeDefinition.CustomAttributes.All(att =>
                        att.AttributeType.FullName
                        != "Microsoft.VisualStudio.TestPlatform.TestSDKAutoGeneratedCode"
                    )
                )
                .ForEach(typeDefinition =>
                {
                    var type = _typeFactory.GetOrCreateTypeFromTypeReference(typeDefinition);
                    var assemblyQualifiedName = System.Reflection.Assembly.CreateQualifiedName(
                        module.Assembly.Name.Name,
                        typeDefinition.FullName
                    );
                    if (
                        !_architectureTypes.ContainsKey(assemblyQualifiedName)
                        && !type.IsCompilerGenerated
                    )
                    {
                        currentTypes.Add(type);
                        _architectureTypes.Add(assemblyQualifiedName, type);
                    }
                });

            _loadTaskRegistry.Add(
                typeof(AddTypesToNamespaces),
                new AddTypesToNamespaces(currentTypes)
            );
        }

        private void UpdateTypeDefinitions()
        {
            _loadTaskRegistry.ExecuteTasks(
                new List<System.Type>
                {
                    typeof(AddMembers),
                    typeof(AddGenericParameterDependencies),
                    typeof(AddAttributesAndAttributeDependencies),
                    typeof(CollectAssemblyAttributes),
                    typeof(AddFieldAndPropertyDependencies),
                    typeof(AddMethodDependencies),
                    typeof(AddGenericArgumentDependencies),
                    typeof(AddClassDependencies),
                    typeof(AddBackwardsDependencies),
                    typeof(AddTypesToNamespaces),
                }
            );
        }

        public Architecture Build()
        {
            var architecture = _architectureCache.TryGetArchitecture(_architectureCacheKey);
            if (architecture != null)
            {
                return architecture;
            }

            UpdateTypeDefinitions();
            var allTypes = _typeFactory.GetAllNonCompilerGeneratedTypes().ToList();
            var genericParameters = allTypes.OfType<GenericParameter>().ToList();
            var referencedTypes = allTypes.Except(Types).Except(genericParameters);
            var namespaces = Namespaces.Where(ns => ns.Types.Any());
            var newArchitecture = new Architecture(
                Assemblies,
                namespaces,
                Types,
                genericParameters,
                referencedTypes
            );
            _architectureCache.Add(_architectureCacheKey, newArchitecture);
            return newArchitecture;
        }
    }
}
